#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import sys
import time
import socket
from heapq import heappush, heappop, heapify

from jujuz import address
from jujuz import defer
from jujuz import log
from jujuz import tcp

import select

def win32select(r, w, e, timeout=None):
    """Win32 select wrapper."""
    if not (r or w):
        # windows select() exits immediately when no sockets
        if timeout is None:
            timeout = 0.01
        else:
            timeout = min(timeout, 0.001)
        sleep(timeout)
        return [], [], []
    # windows doesn't process 'signals' inside select(), so we set a max
    # time or ctrl-c will never be recognized
    if timeout is None or timeout > 0.5:
        timeout = 0.5
    r, w, e = select.select(r, w, w, timeout)
    return r, w + e, []

if os.name == "nt":
    _select = win32select
else:
    _select = select.select

#######################################################################

from time import time as runtimeSeconds

class DelayedCall(object):

    ##implements(IDelayedCall)
    # enable .debug to record creator call stack, and it will be logged if
    # an exception occurs while the function is being run
    debug = False
    _str = None

    def __init__(self, time, func, args, kw, cancel, reset,
                 seconds=runtimeSeconds):
        """
        @param time: Seconds from the epoch at which to call C{func}.
        @param func: The callable to call.
        @param args: The positional arguments to pass to the callable.
        @param kw: The keyword arguments to pass to the callable.
        @param cancel: A callable which will be called with this
            DelayedCall before cancellation.
        @param reset: A callable which will be called with this
            DelayedCall after changing this DelayedCall's scheduled
            execution time. The callable should adjust any necessary
            scheduling details to ensure this DelayedCall is invoked
            at the new appropriate time.
        @param seconds: If provided, a no-argument callable which will be
            used to determine the current time any time that information is
            needed.
        """
        self.time, self.func, self.args, self.kw = time, func, args, kw
        self.resetter = reset
        self.canceller = cancel
        self.seconds = seconds
        self.cancelled = self.called = 0
        self.delayed_time = 0
        if self.debug:
            self.creator = traceback.format_stack()[:-2]

    def getTime(self):
        """Return the time at which this call will fire

        @rtype: C{float}
        @return: The number of seconds after the epoch at which this call is
        scheduled to be made.
        """
        return self.time + self.delayed_time

    def cancel(self):
        """Unschedule this call

        @raise AlreadyCancelled: Raised if this call has already been
        unscheduled.

        @raise AlreadyCalled: Raised if this call has already been made.
        """
        if self.cancelled:
            raise error.AlreadyCancelled
        elif self.called:
            raise error.AlreadyCalled
        else:
            self.canceller(self)
            self.cancelled = 1
            if self.debug:
                self._str = str(self)
            del self.func, self.args, self.kw

    def reset(self, secondsFromNow):
        """Reschedule this call for a different time

        @type secondsFromNow: C{float}
        @param secondsFromNow: The number of seconds from the time of the
        C{reset} call at which this call will be scheduled.

        @raise AlreadyCancelled: Raised if this call has been cancelled.
        @raise AlreadyCalled: Raised if this call has already been made.
        """
        if self.cancelled:
            raise error.AlreadyCancelled
        elif self.called:
            raise error.AlreadyCalled
        else:
            newTime = self.seconds() + secondsFromNow
            if newTime < self.time:
                self.delayed_time = 0
                self.time = newTime
                self.resetter(self)
            else:
                self.delayed_time = newTime - self.time

    def delay(self, secondsLater):
        """Reschedule this call for a later time

        @type secondsLater: C{float}
        @param secondsLater: The number of seconds after the originally
        scheduled time for which to reschedule this call.

        @raise AlreadyCancelled: Raised if this call has been cancelled.
        @raise AlreadyCalled: Raised if this call has already been made.
        """
        if self.cancelled:
            raise error.AlreadyCancelled
        elif self.called:
            raise error.AlreadyCalled
        else:
            self.delayed_time += secondsLater
            if self.delayed_time < 0:
                self.activate_delay()
                self.resetter(self)

    def activate_delay(self):
        self.time += self.delayed_time
        self.delayed_time = 0

    def active(self):
        """Determine whether this call is still pending

        @rtype: C{bool}
        @return: True if this call has not yet been made or cancelled,
        False otherwise.
        """
        return not (self.cancelled or self.called)

    def __le__(self, other):
        return self.time <= other.time

    def __str__(self):
        if self._str is not None:
            return self._str
        if hasattr(self, 'func'):
            if hasattr(self.func, 'func_name'):
                func = self.func.func_name
                if hasattr(self.func, 'im_class'):
                    func = self.func.im_class.__name__ + '.' + func
            else:
                func = reflect.safe_repr(self.func)
        else:
            func = None

        now = self.seconds()
        L = ["<DelayedCall %s [%ss] called=%s cancelled=%s" % (
                id(self), self.time - now, self.called, self.cancelled)]
        if func is not None:
            L.extend((" ", func, "("))
            if self.args:
                L.append(", ".join([reflect.safe_repr(e) for e in self.args]))
                if self.kw:
                    L.append(", ")
            if self.kw:
                L.append(", ".join(['%s=%s' % (k, reflect.safe_repr(v)) for (k, v) in self.kw.iteritems()]))
            L.append(")")

        if self.debug:
            L.append("\n\ntraceback at creation: \n\n%s" % ('    '.join(self.creator)))
        L.append('>')

        return "".join(L)

    # class DelayedCall

class BlockingResolver(object) :
    ##implements(IResolverSimple)

    def getHostByName(self, name, timeout = (1, 3, 11, 45)):
        try:
            address = socket.gethostbyname(name)
        except socket.error:
            msg = "address %r not found" % (name,)
            err = error.DNSLookupError(msg)
            return defer.fail(err)
        else:
            return defer.succeed(address)

    # class BlockingResolver


#######################################################################


class   ISelectReactor(object) :

    resolver = BlockingResolver()

    def __init__(self) :
        self.running = False

        self._pendingTimedCalls = []
        self._newTimedCalls = []
        self._cancellations = 0
        # __init__()

    # IReactorTCP

    def listenTCP(self, port, factory, backlog=50, interface='') :
        factory.reactor = self

        p = tcp.Listener(port, factory, backlog, interface, self)
        p.startListening()
        return p
        # listenTCP()

    def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
        factory.reactor = self

        c = tcp.Connector(host, port, factory, timeout, bindAddress, self)
        c.connect()
        return c

    ###################################################################

    def stop(self):
        """
        See twisted.internet.interfaces.IReactorCore.stop.
        """
        if self._stopped:
            raise error.ReactorNotRunning(
                "Can't stop reactor that isn't running.")
        self._stopped = True
        ##self.callLater(0, self.fireSystemEvent, "shutdown")
        self.callLater(0, self.crash)
        self.callLater(0, self.disconnectAll)

    def crash(self):
        """
        See twisted.internet.interfaces.IReactorCore.crash.
        """
        self.running = False

    def disconnectAll(self):
        """Disconnect every reader, and writer in the system.
        """
        selectables = self.removeAll()
        for reader in selectables:
            try :
                reader.OnConnectionLost(failure.Failure(main.CONNECTION_LOST))
            except KeyboardInterrupt :
                raise
            except :
                log.exception()
        # disconnectAll()

    def run(self) :
        self.startRunning()
        self.mainLoop()
        # run()

    def startRunning(self):
        """
        Method called when reactor starts: do some initialization and fire
        startup events.

        Don't call this directly, call reactor.run() instead: it should take
        care of calling this.
        """
        if self.running: return

        self.running = True
        self._stopped = False
        ##self.fireSystemEvent('startup')
        # startRunning()

    def doIteration(self, delay):
        """Do one iteration over the readers and writers we know about."""
        raise NotImplementedError, 'doIteration() NotImplemented'

    def runUntilCurrent(self) :
        '''run all pending timed calls'''
        # insert new delayed calls now
        self._insertNewDelayedCalls()

        now = self.seconds()
        while self._pendingTimedCalls and (self._pendingTimedCalls[0].time <= now):
            call = heappop(self._pendingTimedCalls)
            if call.cancelled:
                self._cancellations-=1
                continue

            if call.delayed_time > 0:
                call.activate_delay()
                heappush(self._pendingTimedCalls, call)
                continue

            try:
                class_name = ''
                if hasattr(call.func, 'im_class') : class_name = call.func.im_class.__name__ + '.'
                log.debug('delayed calling %s%s(args=%s, kwargs=%s)', class_name, call.func.func_name, call.args, call.kw)
                call.called = 1
                call.func(*call.args, **call.kw)
            except Exception, why:
                log.exception()
                log.error('runUntilCurrent Exception: %s', why)
                ##log.deferr()
                if hasattr(call, "creator"):
                    e = "\n"
                    e += " C: previous exception occurred in " + \
                         "a DelayedCall created here:\n"
                    e += " C:"
                    e += "".join(call.creator).rstrip().replace("\n","\n C:")
                    e += "\n"
                    log.msg(e)


        if (self._cancellations > 50 and
             self._cancellations > len(self._pendingTimedCalls) >> 1):
            self._cancellations = 0
            self._pendingTimedCalls = [x for x in self._pendingTimedCalls
                                       if not x.cancelled]
            heapify(self._pendingTimedCalls)
        # runUntilCurrent()

    def mainLoop(self):
        while self.running:
            try:
                while self.running:
                    # Advance simulation time in delayed event
                    # processors.
                    self.runUntilCurrent()
                    t2 = self.timeout()
                    t = self.running and t2
                    self.doIteration(t)
            except Exception, why:
                log.exception()
                log.error("Unexpected error in main loop : %s.", why)
                time.sleep( 1 )
            else:
                log.error('Main loop terminated.')
        # mainLoop()

    ###################################################################

    def timeout(self) :
        # to-be-done
        return  None

    seconds = staticmethod(runtimeSeconds)

    ###################################################################

    def callLater(self, _seconds, _f, *args, **kw):
        """See twisted.internet.interfaces.IReactorTime.callLater.
        """
        assert callable(_f), "%s is not callable" % _f
        assert sys.maxint >= _seconds >= 0, \
               "%s is not greater than or equal to 0 seconds" % (_seconds,)
        tple = DelayedCall(self.seconds() + _seconds, _f, args, kw,
                           self._cancelCallLater,
                           self._moveCallLaterSooner,
                           seconds=self.seconds)
        self._newTimedCalls.append(tple)
        return tple

    def _moveCallLaterSooner(self, tple):
        # Linear time find: slow.
        heap = self._pendingTimedCalls
        try:
            pos = heap.index(tple)

            # Move elt up the heap until it rests at the right place.
            elt = heap[pos]
            while pos != 0:
                parent = (pos-1) // 2
                if heap[parent] <= elt:
                    break
                # move parent down
                heap[pos] = heap[parent]
                pos = parent
            heap[pos] = elt
        except ValueError:
            # element was not found in heap - oh well...
            pass

    def _cancelCallLater(self, tple):
        self._cancellations+=1

    def _insertNewDelayedCalls(self):
        for call in self._newTimedCalls:
            if call.cancelled:
                self._cancellations-=1
            else:
                call.activate_delay()
                heappush(self._pendingTimedCalls, call)
        self._newTimedCalls = []


    ###################################################################

    def resolve(self, name, timeout = (1, 3, 11, 45)):
        """Return a Deferred that will resolve a hostname.
        """
        if not name:
            # XXX - This is *less than* '::', and will screw up IPv6 servers
            return defer.succeed('0.0.0.0')
        if address.isIPAddress(name):
            return defer.succeed(name)
        return self.resolver.getHostByName(name, timeout)

    ###################################################################

    def _removeAll(self, readers, writers):
        """
        Remove all readers and writers, and return list of Selectables.

        Meant for calling from subclasses, to implement removeAll, like::

          def removeAll(self):
              return self._removeAll(reads, writes)

        where C{reads} and C{writes} are iterables.
        """
        readers = [reader for reader in readers if
                   reader is not self.waker]

        readers_dict = {}
        for reader in readers:
            readers_dict[reader] = 1

        for reader in readers:
            self.removeReader(reader)
            self.removeWriter(reader)

        writers = [writer for writer in writers if
                   writer not in readers_dict]
        for writer in writers:
            self.removeWriter(writer)

        return readers+writers
        # _removeAll()

    # class ISelectReactor


class   SelectReactor(ISelectReactor) :

    def __init__(self) :
        ISelectReactor.__init__(self)

        self._reads = {}
        self._writes = {}
        # __init__()

    ###################################################################

    def _preenDescriptors(self):
        log.error("Malformed file descriptor found.  Preening lists.")
        readers = self._reads.keys()
        writers = self._writes.keys()
        self._reads.clear()
        self._writes.clear()
        for selDict, selList in ((self._reads, readers),
                                 (self._writes, writers)):
            for selectable in selList:
                try:
                    select.select([selectable], [selectable], [selectable], 0)
                except Exception, e:
                    log.msg("bad descriptor %s" % selectable)
                    self._disconnectSelectable(selectable, e, False)
                else:
                    selDict[selectable] = 1
        # _preenDescriptors

    def doSelect(self, timeout):
        """
        Run one iteration of the I/O monitor loop.

        This will run all selectables who had input or output readiness
        waiting for them.
        """
        while 1:
            try:
                log.debug('timeout=%s size-readset=%d size-writeset=%d', `timeout`, len(self._reads), len(self._writes))
                r, w, ignored = _select(self._reads.keys(),
                                        self._writes.keys(),
                                        [], timeout)
                break
            except ValueError, ve:
                # Possibly a file descriptor has gone negative?
                log.error('doSelect Exception : %s', ve)
                self._preenDescriptors()
            except TypeError, te:
                # Something *totally* invalid (object w/o fileno, non-integral
                # result) was passed
                log.error('doSelect Exception : %s', te)
                self._preenDescriptors()
            except (select.error, IOError), se:
                # select(2) encountered an error
                if se.args[0] in (0, 2):
                    # windows does this if it got an empty list
                    if (not self._reads) and (not self._writes):
                        return
                    else:
                        raise
                elif se.args[0] == EINTR:
                    return
                elif se.args[0] == EBADF:
                    self._preenDescriptors()
                else:
                    # OK, I really don't know what's going on.  Blow up.
                    raise

        log.debug('timeout=%s count-read=%d count-write=%d', `timeout`, len(r), len(w))
        for selectable in r :
            log.debug('doREAD %s', `selectable`)
            selectable.doRead()
        for selectable in w :
            log.debug('doWRITE %s', `selectable`)
            selectable.doWrite()
        # doSelect()

    doIteration = doSelect


    ###################################################################

    def addReader(self, reader):
        """
        Add a FileDescriptor for notification of data available to read.
        """
        log.debug('enableREAD %s', `reader`)
        self._reads[reader] = 1

    def addWriter(self, writer):
        """
        Add a FileDescriptor for notification of data available to write.
        """
        log.debug('enableWRITE %s', `writer`)
        self._writes[writer] = 1

    def removeReader(self, reader):
        """
        Remove a Selectable for notification of data available to read.
        """
        log.debug('disableREAD %s', `reader`)
        if reader in self._reads:
            del self._reads[reader]

    def removeWriter(self, writer):
        """
        Remove a Selectable for notification of data available to write.
        """
        log.debug('disableWRITE %s', `writer`)
        if writer in self._writes:
            del self._writes[writer]

    def removeAll(self):
        return self._removeAll(self._reads, self._writes)


    def getReaders(self):
        return self._reads.keys()


    def getWriters(self):
        return self._writes.keys()

    # class SelectReactor


Reactor = SelectReactor

#######################################################################



